package com.sbtr.business.publishtask.process;

import com.alibaba.fastjson.JSONObject;
import com.sbtr.business.importmodel.entity.ImportModelTask;
import com.sbtr.business.publishtask.dto.PublishServerStatisticDto;
import com.sbtr.business.publishtask.entity.PublishTaskData;
import com.sbtr.business.publishtask.service.IPublishAssignDataService;
import com.sbtr.business.trainingmodel.entity.TrainingModelData;
import com.sbtr.business.trainingserver.entity.TrainingServer;
import com.sbtr.business.trainingserver.service.IPublishServerHttpApi;
import com.sbtr.business.trainingserver.service.ITrainingServerService;
import com.sbtr.business.trainingtask.entity.TrainingInfo;
import com.sbtr.business.trainingtask.service.ITrainingInfoService;
import com.sbtr.constant.ProjectConstant;
import com.sbtr.exception.BizException;
import com.sbtr.open.importmodel.dto.ImportModelTaskDto;
import com.sbtr.open.publishtask.dto.PublishTaskDataDto;
import com.sbtr.open.trainingmodel.dto.TrainingModelDataDto;
import com.sbtr.open.trainingserver.dto.TrainingServerDto;
import com.sbtr.open.trainingtask.dto.TrainingInfoDto;
import com.sbtr.util.BeanCopyUtils;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * 第三方服务调用的processor
 *
 * @author qiujun
 * @date 2019-10-29
 */
@Service
public class ServerClientProcessor {

	private static final Logger logger = LoggerFactory.getLogger(ServerClientProcessor.class);

	@Autowired
	private ITrainingServerService trainingServerService;

	@Autowired
	private IPublishAssignDataService publishAssignService;

	@Autowired
	private IPublishServerHttpApi publishServerHttpApi;



	@Autowired
	private ITrainingInfoService trainingInfoService;



	@Autowired
	private RestTemplate restTemplate;

	/**
	 * 将模型发布到推理服务器上<br>
	 * 方法会按照指定的策略选择一个最优的推理服务器资源发布<br>
	 * 如果都没有可用的资源，会抛出exception<br>
	 * 目前该接口的实现在高并发的情况下会存在性能问题，因为开发时间等原因，后期在调整优化
	 *
	 * @return 调用成功的推理服务器资源信息
	 * @author qiujun
	 * @date 2019-10-29
	 */


	/**
	 * 和publishModel类似，但针对数据类的模型部署相关进行了优化
	 */
	public TrainingServer publishModelData(TrainingModelData model, PublishTaskData task) {
		// 1. 选择一台最优资源服务器，用于模型发布调用
		TrainingServer server = getOptimalPublishData();
		if (null == server) {
			throw new BizException("没有可用的服务器");
		}

		// 2. 调用
//		TrainingTaskData trainingTask = trainingTaskDataService.getById(model.getTaskId());
		TrainingInfo trianingInfo=trainingInfoService.getById(model.getTaskId());

		TrainingServerDto serverDto = BeanCopyUtils.copyProperties(server, TrainingServerDto.class);

		TrainingInfoDto trainingInfoDto = BeanCopyUtils.copyProperties(trianingInfo, TrainingInfoDto.class);


		PublishTaskDataDto publishTaskDataDto = BeanCopyUtils.copyProperties(task, PublishTaskDataDto.class);
		TrainingModelDataDto trainingModelDto = BeanCopyUtils.copyProperties(model, TrainingModelDataDto.class);
	   //发布模型
		boolean clientResult = publishServerHttpApi.addTaskData2(serverDto, trainingInfoDto, publishTaskDataDto,
				trainingModelDto);
		if (!clientResult) {
			throw new BizException("调用发布接口失败");
		}
		// 3. 封装返回结果
		return server;
	}

	/**
	 * 发布外部导入的模型
	 * @param model
	 * @return
	 */
	public TrainingServer publishExternalModel2(TrainingModelData model, PublishTaskData task) {

		// 1. 选择一台最优资源服务器，用于模型发布调用
		TrainingServer server = getOptimalPublishData();
		if (null == server) {
			throw new BizException("没有可用的服务器");
		}

		// 2. 调用

		TrainingServerDto serverDto = BeanCopyUtils.copyProperties(server, TrainingServerDto.class);
		PublishTaskDataDto publishTaskDataDto = BeanCopyUtils.copyProperties(task, PublishTaskDataDto.class);
		TrainingModelDataDto trainingModelDto = BeanCopyUtils.copyProperties(model, TrainingModelDataDto.class);
		//发布模型
		boolean clientResult = publishServerHttpApi.publishExternalModel2(serverDto, publishTaskDataDto, trainingModelDto);
//		boolean clientResult = publishServerHttpApi.addTaskData2(serverDto, publishTaskDataDto, trainingModelDto);
		if (!clientResult) {
			throw new BizException("调用发布接口失败");
		}
		// 3. 封装返回结果
		return server;
	}

	/**
	 * 发布外部导入的模型
	 * @param model
	 * @return
	 */
	public String publishExternalModel(ImportModelTask model) {
		// 1. 选择一台最优资源服务器，用于模型发布调用
		TrainingServer server = getOptimalPublishData();
		if (null == server) {
			throw new BizException("没有可用的服务器");
		}
		TrainingServerDto serverDto = BeanCopyUtils.copyProperties(server,TrainingServerDto.class);
		ImportModelTaskDto modelDto = BeanCopyUtils.copyProperties(model,ImportModelTaskDto.class);

		String result = publishServerHttpApi.publishExternalModel(serverDto,modelDto);
		logger.info("发布外部导入的模型结果：{}",result);
		return result;
	}

	/**
	 * 下线外部导入的模型
	 * @param model
	 * @return
	 */
	public boolean offLineExternalModel(ImportModelTask model) {
		// 1. 选择一台最优资源服务器，用于模型发布调用
		TrainingServer server = getOptimalPublishData();
		if (null == server) {
			throw new BizException("没有可用的服务器");
		}
		TrainingServerDto serverDto = BeanCopyUtils.copyProperties(server,TrainingServerDto.class);
		ImportModelTaskDto modelDto = BeanCopyUtils.copyProperties(model,ImportModelTaskDto.class);

		boolean result = publishServerHttpApi.stopTaskData(serverDto, model.getId());
		return result;
	}

	/**
	 * 从推理服务器列表中选择最优的一个资源，用于发布
	 *
	 * @return
	 * @author qiujun
	 * @date 2019-10-30
	 */
	public TrainingServer getOptimalPublish() {
		// 1. 获取所有在线的推理服务器列表
		List<TrainingServer> list = trainingServerService.listAllPublish();
		if (CollectionUtils.isEmpty(list)) {
			throw new BizException("没有上线的服务器列表");
		}
		// 实时检测服务器是否在线,把不在线的排除
		Iterator<TrainingServer> iterator = list.iterator();
		while (iterator.hasNext()) {
			TrainingServer next = iterator.next();
			if (!publishServerHttpApi.serverIsOnline(BeanCopyUtils.copyProperties(next, TrainingServerDto.class),
					ProjectConstant.PROJECT_TYPE_IMG_CLASSIFY)) {
				iterator.remove();
			}
		}
		if (CollectionUtils.isEmpty(list)) {
			throw new BizException("没有可用的gpu服务器");
		}

		// 2. 获取这批推理服务器当前任务资源使用情况
		List<Long> serverIds = list.stream().map(TrainingServer::getId).collect(Collectors.toList());
		List<PublishServerStatisticDto> statistics = publishAssignService.countByServer(serverIds);

		/**
		 * 策略规则 1. 若这批服务器还存在没有任务的，优先选择那台 2. 如果都跑了任务，选择任务数量最小的那台
		 */
		// 策略1
		if (CollectionUtils.isEmpty(statistics)) {
			return list.get(0);
		}
		Set<Long> statisticSet = statistics.stream().map(PublishServerStatisticDto::getServerId)
				.collect(Collectors.toSet());
		for (TrainingServer server : list) {
			if (!statisticSet.contains(server.getId()))
				return server;
		}

		// 策略2
		statistics.sort((o1, o2) -> (int) (o1.getNumber() - o2.getNumber()));
		PublishServerStatisticDto dto = statistics.get(0);
		for (TrainingServer server : list) { // 数据不多，O2次方时间复杂度也接受
			if (server.getId().equals(dto.getServerId()))
				return server;
		}
		return null;
	}

	public TrainingServer getOptimalPublishData() {
		// 1. 获取所有在线的推理服务器列表
		List<TrainingServer> list = trainingServerService
				.listPublishByServerType(TrainingServerDto.SERVER_TYPE_DATA_PUBLISH);
		if (CollectionUtils.isEmpty(list)) {
			throw new BizException("没有上线的服务器列表");
		}
		// 实时检测服务器是否在线,把不在线的排除
		Iterator<TrainingServer> iterator = list.iterator();
		while (iterator.hasNext()) {
			TrainingServer next = iterator.next();
			if (!publishServerHttpApi.serverIsOnline(BeanCopyUtils.copyProperties(next, TrainingServerDto.class),
					ProjectConstant.PROJECT_TYPE_DATA_PREDICT)) {
				iterator.remove();
			}
		}
		if (CollectionUtils.isEmpty(list)) {
			throw new BizException("没有可用的服务器");
		}

		// 2. 获取这批推理服务器当前任务资源使用情况
		List<Long> serverIds = list.stream().map(TrainingServer::getId).collect(Collectors.toList());
		List<PublishServerStatisticDto> statistics = publishAssignService.countByServer(serverIds);

		/**
		 * 策略规则 1. 若这批服务器还存在没有任务的，优先选择那台 2. 排除服务器内存资源使用达到85的服务器，并选择
		 * 内存资源使用最少的服务器，误差为3% 2. 在内存使用资源最少的一批服务器中选择服务数量最少的服务
		 */
		// 策略1
		if (CollectionUtils.isEmpty(statistics)) {
			return list.get(0);
		}
		Set<Long> statisticSet = statistics.stream().map(PublishServerStatisticDto::getServerId)
				.collect(Collectors.toSet());
		for (TrainingServer server : list) {
			if (!statisticSet.contains(server.getId()))
				return server;
		}

		// 策略2
		List<PublishServerStatisticDto> filterStatistics = new ArrayList<PublishServerStatisticDto>();
		for (TrainingServer server : list) {
			Double used = this.getMemoryUsed(server);
			if (used < 85) {
				for (PublishServerStatisticDto dto : statistics) {
					if (dto.getServerId().equals(server.getId())) {
						dto.setMemUsedPer(used);
						filterStatistics.add(dto);
						break;
					}
				}
			}
		}
		//
		if (filterStatistics.size() == 0) {
			return null;
		} else if (filterStatistics.size() == 1) {
			for (TrainingServer server : list) {
				if (filterStatistics.get(0).getServerId().equals(server.getId()))
					return server;
			}
		}

		// 策略3
		filterStatistics.sort((o1, o2) -> (int) (o1.getMemUsedPer() - o1.getMemUsedPer()));
		List<PublishServerStatisticDto> filterMinStatistics = new ArrayList<PublishServerStatisticDto>();
		PublishServerStatisticDto minPSSDto = filterStatistics.get(0);
		filterMinStatistics.add(minPSSDto);
		for (int i = 1; i < filterStatistics.size(); i++) {
			if (filterStatistics.get(i).getMemUsedPer() - minPSSDto.getMemUsedPer() > 3.0) {
				break;
			} else {
				filterMinStatistics.add(filterStatistics.get(i));
			}
		}

		filterMinStatistics.sort((o1, o2) -> (int) (o1.getNumber() - o2.getNumber()));
		PublishServerStatisticDto dto = statistics.get(0);
		for (TrainingServer server : list) { // 数据不多，O2次方时间复杂度也接受
			if (server.getId().equals(dto.getServerId()))
				return server;
		}
		return null;
	}

	/**
	 * 调用推理服务器API获取当前内存使用量
	 */
	public Double getMemoryUsed(TrainingServer server) {
		String url = "http://" + server.getIp() + "/info/getMemoryUsed";
		logger.info("启动部署任务请求地址: {}", url);
		ResponseEntity<String> getForEntity = restTemplate.getForEntity(url, String.class);
		String body = getForEntity.getBody();
		JSONObject jsObj = JSONObject.parseObject(body);

		if (jsObj.getInteger("code") != null && 200 == jsObj.getInteger("code")) {
			JSONObject res = jsObj.getJSONObject("data");
			return res.getDouble("used");
		} else {
			Integer code = jsObj.getInteger("code") != null ? jsObj.getInteger("code") : 0;
			String msg = StringUtils.isNotBlank(jsObj.getString("msg")) ? jsObj.getString("msg") : "";
			String data = StringUtils.isNotBlank(jsObj.getString("data")) ? jsObj.getString("data") : "";
			logger.error("状态码:{}, msg:{},data:{} ", code, msg, data);
			String info = "msg: " + msg + ", data: " + data;
			throw new BizException(info);
		}
	}
}
